/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package parser;

import codegen.CodeGen;
import parser.nodes.*;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import scanner.Scanner;
import scanner.Token;
import static scanner.TokenType.*;

public class Parser {

    private String filepath;
    private Scanner scanner;
    private Token currentToken = null;
    private Token nextToken = null;
    private List<Token> backBuffer = new LinkedList<>();
    private final int bufferSize = 10;
    private int bufferPosition = -1;

    public Parser(String filepath) throws FileNotFoundException {
        this.filepath = filepath;
        this.scanner = new Scanner(new FileReader(filepath));
    }

    private void inSymbol() throws IOException {
        currentToken = nextToken;

        if (bufferPosition == -1) {
            nextToken = scanner.nextToken();
            backBuffer.add(nextToken);
            if (backBuffer.size() > bufferSize) {
                backBuffer.remove(0);
            }
        } else {
            nextToken = backBuffer.get((backBuffer.size() - 1 - bufferPosition--));
        }
    }

    private void outSymbol() {
        ++bufferPosition;
        if (bufferPosition >= backBuffer.size() - 1) {
            --bufferPosition;
            currentToken = nextToken = null;
        } else {
            nextToken = currentToken;
            currentToken = backBuffer.get(backBuffer.size() - 3 - bufferPosition);
        }
    }

    /**
     * Wenn ein keyword/operator an einer bestimmten Stelle stehen muss, damit
     * der Code gültig ist, dann wird dies durch diesen Aufruf sichergestellt.
     * Nach dem Aufruf befindet sich nextToken auf dem Token nach dem keyword.
     *
     * @param keyword das erwartete Schlüsselwort/Operator
     * @throws ParseException Wenn das Zeichen nicht das erwartete ist
     * @throws IOException
     */
    private void require(String keyword) throws ParseException, IOException {
        if (nextToken.is(keyword)) {
            inSymbol();
        } else {
            error("Expected a '" + keyword + "', but found '" + nextToken + "'");
        }
    }

    public void parse() throws IOException, ParseException, CompileError {
		inSymbol();
		Node syntaxTree = Module();
		CodeGen c = new CodeGen("src/output/prog.asm");
		c.compile(syntaxTree);
    }

    private void error(String s) throws ParseException {
        throw new ParseException(s + " at " + nextToken.line() + ":" + nextToken.col());
    }

    private FactorNode Factor() throws IOException, ParseException {
        if (nextToken.equals("(")) {
            inSymbol();
            ExpressionNode exp = Expression();
            require(")");
            return new FactorNode(exp);
        } else if (nextToken.type() == INTEGER) {
            inSymbol();
            return new FactorNode(currentToken);
        } else if (nextToken.type() == IDENTIFIER) {
            inSymbol();
            Token temp = currentToken;
            SelectorNode s = Selector();
            return new FactorNode(temp, s);
        } else if (nextToken.type() == STRING) {
            inSymbol();
            return new FactorNode(currentToken);
        }
        return new FactorNode(Read());
    }

    private ExpressionNode Expression() throws IOException, ParseException {
        SimpleExpressionNode left = SimpleExpression();
        if (nextToken.is("=") || nextToken.is("#") || nextToken.is("<") || nextToken.is("<=") || nextToken.is(">") || nextToken.is(">=")) {
            Token comparison = nextToken;
            inSymbol();
            SimpleExpressionNode right = SimpleExpression();
            return new ExpressionNode(left, comparison, right);
        }
        return new ExpressionNode(left);
    }

    private SimpleExpressionNode SimpleExpression() throws IOException, ParseException {
        Token minus = null;
        if (nextToken.is("-")) {
            minus = nextToken;
            inSymbol();
        }
        TermNode term = Term();
        SimpleExpressionNode sExpr = new SimpleExpressionNode(minus, term);
        while (nextToken.is("-") || nextToken.is("+")) {
            Token operator = nextToken;
            inSymbol();
            sExpr.addTerm(operator, Term());
        }
        return sExpr;
    }

    private SelectorNode Selector() throws IOException, ParseException {
        SelectorNode selector = new SelectorNode();
        while (nextToken.is(".") || nextToken.is("[")) {
            inSymbol();
            if (currentToken.is(".")) {
                if (nextToken.type() == IDENTIFIER) {
                    selector.addSelector(nextToken);
                    inSymbol();
                } else {
                    error("Expected identifier, but found " + nextToken);
                }
            } else { //[
                selector.addSelector(Expression());
                require("]");
            }
        }
        return selector;
    }

    private TermNode Term() throws IOException, ParseException {
        FactorNode factor = Factor();
        TermNode term = new TermNode(factor);

        while (nextToken.is("*") || nextToken.is("/")) {
            Token operator = nextToken;
            inSymbol();
            term.addFactor(operator, Factor());
        }
        return term;
    }

    private ReadNode Read() throws ParseException, IOException {
        require("READ");
        if (nextToken.type() == STRING) {
            inSymbol();
            return new ReadNode(currentToken);
        }
        return new ReadNode();
    }

    private IndexExpressionNode IndexExpression() throws ParseException, IOException {
        if (nextToken.type() == INTEGER) {
            inSymbol();
            return new IndexExpressionNode(currentToken);
        } else if (nextToken.type() == IDENTIFIER) {
            inSymbol();
            return new IndexExpressionNode(currentToken);
        }
        error("Expected an IndexExpression, but found: " + nextToken);
        return null;
    }

    private AssignmentNode Assignment() throws IOException, ParseException {
        if (nextToken.type() == IDENTIFIER) {
            Token ident = nextToken;
            inSymbol();
            SelectorNode selector = Selector();
            require(":=");
            return new AssignmentNode(ident, selector, Expression());
        }
        return null;
    }

    private ActualParametersNode ActualParameters() throws IOException, ParseException {
        ActualParametersNode params = new ActualParametersNode(Expression());
        while (nextToken.is(",")) {
            inSymbol();
            params.addExpression(Expression());
        }
        return params;
    }

    private ProcedureCallNode ProcedureCall() throws IOException, ParseException {
        if (nextToken.type() == IDENTIFIER) {
            Token ident = nextToken;
            inSymbol();
            if (nextToken.is("(")) {
                inSymbol();
                if (nextToken.is(")")) {
                    inSymbol();
                    return new ProcedureCallNode(ident);
                } else {
                    ActualParametersNode params = ActualParameters();
                    require(")");
                    return new ProcedureCallNode(ident, params);
                }
            } else { //Könnte eine Zuweisung sein
                outSymbol();
                return null;
            }
        }
        return null;
    }

    private StatementNode Statement() throws IOException, ParseException {
        Node stmt;
        if (nextToken.is("PRINT")) {
            inSymbol();
            stmt = new PrintNode(Expression());
        } else if (nextToken.is("IF")) {
            stmt = IfStatement();
        } else if (nextToken.is("WHILE")) {
            stmt = WhileStatement();
        } else if (nextToken.is("REPEAT")) {
            stmt = RepeatStatement();
        } else {
            stmt = ProcedureCall();
            if (stmt == null) {
                stmt = Assignment();
            }
        }
        return new StatementNode(stmt);
    }

    private StatementSequenceNode StatementSequence() throws IOException, ParseException {
        StatementSequenceNode seq = new StatementSequenceNode(Statement());
        while (nextToken.is(";")) {
            inSymbol();
            seq.add(Statement());
        }
        return seq;
    }

    private IfStatementNode IfStatement() throws IOException, ParseException {
        require("IF");
        ExpressionNode condition = Expression();
        require("THEN");
        IfStatementNode ifNode = new IfStatementNode(condition, StatementSequence());
        while (nextToken.is("ELSIF")) {
            inSymbol();
            condition = Expression();
            require("THEN");
            ifNode.addElseIf(condition, StatementSequence());
        }
        if (nextToken.is("ELSE")) {
            inSymbol();
            ifNode.setElse(StatementSequence());
        }
        require("END");
        return ifNode;
    }

    private WhileStatementNode WhileStatement() throws IOException, ParseException {
        require("WHILE");
        ExpressionNode condition = Expression();
        require("DO");
        WhileStatementNode whileNode = new WhileStatementNode(condition, StatementSequence());
        require("END");
        return whileNode;
    }

    private RepeatStatementNode RepeatStatement() throws IOException, ParseException {
        require("REPEAT");
        StatementSequenceNode stmts = StatementSequence();
        require("UNTIL");
        return new RepeatStatementNode(stmts, Expression());
    }

    private ModuleNode Module() throws IOException, ParseException {
        require("MODULE");
        if (nextToken.type() == IDENTIFIER) {
            Token moduleName = nextToken;
            inSymbol();
            require(";");
            DeclarationsNode delcs = Declarations();
            require("BEGIN");
            StatementSequenceNode stmts = StatementSequence();
            require("END");
            if (nextToken.type() == IDENTIFIER) {
                if (moduleName.equals(nextToken)) {
                    inSymbol();
                    require(".");
                    return new ModuleNode(moduleName, delcs, stmts);
                } else
                {
                    error("Module identifier at END doesn't match the module's definition. Module is defined with " + moduleName + ", but found " + nextToken);
                }
            } else {
                error("Expected an identifier after END, but found: " + nextToken);
            }
        } else {
            error("Expected a module identifier, but found: " + nextToken);
        }
        return null;
    }

    private DeclarationsNode Declarations() throws IOException, ParseException {
        DeclarationsNode decls = new DeclarationsNode();
        if (nextToken.is("CONST")) {
            inSymbol();
            do {
                Token ident = nextToken;
                inSymbol();
                require("=");
                if (nextToken.type() == INTEGER || nextToken.type() == IDENTIFIER)
                    decls.addConstant(ident, nextToken);
                else
                    error("Constant " + ident + " cannot be initialized. Expected constant, but found: " + nextToken);
                inSymbol();
                require(";");
            } while (nextToken.type() == IDENTIFIER);
        }

        if (nextToken.is("TYPE")) {
            inSymbol();
            do {
                Token ident = nextToken;
                inSymbol();
                require("=");
                decls.addType(ident, Type());
                require(";");
            } while (nextToken.type() == IDENTIFIER);
        }

        if (nextToken.is("VAR")) {
            inSymbol();
            do {
                IdentListNode identList = IdentList();
                require(":");
                decls.addVar(identList, Type());
                require(";");
            } while (nextToken.type() == IDENTIFIER);
        }

        while (nextToken.is("PROCEDURE")) {
            decls.addProcDecl(ProcedureDeclaration());
            require(";");
        }

        return decls;
    }

    private ProcedureDeclarationNode ProcedureDeclaration() throws IOException, ParseException {
        ProcedureHeadingNode signature = ProcedureHeading();
        require(";");
        ProcedureBodyNode body = ProcedureBody();
        if (nextToken.type() == IDENTIFIER) {
            Token procName = nextToken;
            if (procName.equals(signature.getProcName())) {
                inSymbol();
                return new ProcedureDeclarationNode(signature, body);
            } else {
                error("The procedure name after END doesn't match the procedure's name. Expected '" + signature.getProcName() + "' but found '" + procName + "'");
            }
        } else {
            error("Expected a procedure identifier in procedure declaration, but found: " + nextToken);
        }
        return null;
    }

    private TypeNode Type() throws IOException, ParseException {
        if (nextToken.type() == IDENTIFIER) {
            inSymbol();
            return new TypeNode(currentToken);
        } else if (nextToken.is("ARRAY")) {
            return new TypeNode(ArrayType());
        } else if (nextToken.is("RECORD")) {
            return new TypeNode(RecordType());
        } else {
            error("Expected a type, but found: " + nextToken);
            return null;
        }
    }

    private IdentListNode IdentList() throws IOException, ParseException {
        if (nextToken.type() == IDENTIFIER) {
            IdentListNode list = new IdentListNode(nextToken);
            inSymbol();
            while (nextToken.is(",")) {
                inSymbol();
                list.addIdent(nextToken);
                inSymbol();
            }
            return list;
        } else {
            error("Identifier list expected, but found: " + nextToken);
            return null;
        }
    }

    private ProcedureHeadingNode ProcedureHeading() throws IOException, ParseException {
        require("PROCEDURE");
        if (nextToken.type() == IDENTIFIER) {
            Token procName = nextToken;
            inSymbol();
            require("(");
            if (nextToken.is(")")) {
                inSymbol();
                return new ProcedureHeadingNode(procName);
            } else {
                ProcedureHeadingNode procNode = new ProcedureHeadingNode(procName, FormalParameters());
                require(")");
                return procNode;
            }
        } else {
            error("Expected a procedure name in procedure declaration, but found: " + nextToken);
        }
        return null;
    }

    private ProcedureBodyNode ProcedureBody() throws IOException, ParseException {
        DeclarationsNode decls = Declarations();
        require("BEGIN");
        StatementSequenceNode stmts = StatementSequence();
        require("END");
        return new ProcedureBodyNode(decls, stmts, nextToken);
    }

    private ArrayTypeNode ArrayType() throws IOException, ParseException {
        inSymbol(); // ARRAY überlesen
        require("[");
        IndexExpressionNode iNode = IndexExpression();
        require("]");
        require("OF");
        return new ArrayTypeNode(iNode, Type());
    }

    private RecordTypeNode RecordType() throws IOException, ParseException {
        require("RECORD");
        if (nextToken.is("END")) {
            inSymbol();
            return new RecordTypeNode(); //RECORD END ist erlaubt -.- WTF???
        }
        RecordTypeNode record = new RecordTypeNode(FieldList());
        while (nextToken.is(";")) {
            inSymbol();
			if (nextToken.is("END"))
				break;
            record.addFieldList(FieldList());
        }
        require("END");
        return record;
    }

    private FormalParametersNode FormalParameters() throws IOException, ParseException {
        FormalParametersNode fpNode = new FormalParametersNode(FPSection());
        while (nextToken.is(";")) {
            inSymbol();
            fpNode.addSection(FPSection());
        }
        return fpNode;
    }

    private FPSectionNode FPSection() throws IOException, ParseException {
        boolean isVar = false;
        if (nextToken.is("VAR")) {
            inSymbol();
            isVar = true;
        }
        IdentListNode idents = IdentList();
        require(":");
        return new FPSectionNode(isVar, idents, Type());
    }

    private FieldListNode FieldList() throws IOException, ParseException {
        IdentListNode idents = IdentList();
        require(":");
        return new FieldListNode(idents, Type());
    }
}

